#!/usr/bin/env python
# -*-coding:utf-8 -*-
# @file       partitions.py
# @author     yearnext
# @version    1.0.0
# @date       2022-04-18
# @brief      partitions component

# Import -----------------------------------------------------------------------
import os
import stat
import csv
import string
import time

# Define -----------------------------------------------------------------------
# export class
__all__ = [
    'PartitionsItem',
    'PartitionsInfo',
    # 'TemplateFileDir',
    # 'PartitionsTableList',
    # 'IcfTemplateFilePath',
    # 'PartitionsAttrDict',
    # 'PartitionsTemplateDict',
]

# Data -------------------------------------------------------------------------
# define partitions file template folder base dir
TemplateFileDir = os.path.split(os.path.realpath(__file__))[0] + '\\filetemplate\\'
# print(TemplateFileDir)

# define partitions list
PartitionsTableHead = [
    'name',
    'attribute',
    'device',
    'location',
    'page',
    'begin',
    'size',
    'verify',
    'partindex',
    'fal',
    'needlink',
]

# define ICF Template File Path
IcfTemplateFilePath = [
    ['link_icf\\link_def_ram', 'def_ram'],
    ['link_icf\\link_def_rom', 'def_rom'],
    ['link_icf\\link_def_intvec', 'def_intvec'],
    ['link_icf\\link_def_symbol', 'def_symbol'],
    ['link_icf\\link_def_ram', 'def_ram'],
    ['link_icf\\link_def_region', 'def_region'],
    ['link_icf\\link_def_stack_size', 'def_stack_size'],
    ['link_icf\\link_def_heap_size', 'def_heap_size'],
    ['link_icf\\link_def_place_in_section', 'def_place_in_section'],
    ['link_icf\\link_def_place_at_section', 'def_place_at_section'],
    ['link_icf\\link_icf', 'def_icf'],
    ['firmware_info\\c_file', 'def_fw_info_c'],
    ['firmware_info\\h_file', 'def_fw_info_h'],
    ['firmware_info\\param', 'def_fw_info_param'],
    ['firmware_info\\internal_fal_func', 'def_fal_func'],
    ['firmware_info\\export_fal_internal_dev', 'def_export_fal_internal_dev'],
    ['headfile_macro', 'def_h_macro'],
]

# Init Env ---------------------------------------------------------------------
# partitions dict init
PartitionsTableList = []

for i in range(0, len(PartitionsTableHead)):
    PartitionsTableList.append([PartitionsTableHead[i], i])

# partitions attribute dict init
PartitionsAttrDict = {}

PartitionsAttrDict.clear()
# PartitionsAttrDict['title'] = PartitionsTableList
for temp in PartitionsTableList:
    PartitionsAttrDict[temp[0]] = temp[1]

# define icf template dict
PartitionsTemplateDict = {}

for temp in IcfTemplateFilePath:
    with open(TemplateFileDir + temp[0] + '.template', 'r') as f:
        PartitionsTemplateDict[temp[1]] = f.read()


# Function ---------------------------------------------------------------------
def getCurTimeString(format_type=0):
    """
    current time to string
    in: time output format
    out: time string
    """
    if format_type == 1:
        fmt = '%Y-%m-%d'
    elif format_type == 2:
        fmt = '%H:%M:%S'
    else:
        fmt = '%Y-%m-%d %H:%M:%S'

    return time.strftime(fmt, time.localtime())


def str2hexval(param):
    """
    hex value string to int value
    in: [param] input hex value
    out: int value
    """
    if len(param) == 0:
        return None

    temp_str = param.replace(' ', '')
    temp_str = temp_str.lower()

    if not temp_str.isalnum():
        return None

    # val type check
    if len(temp_str) > 2 and temp_str[0] == '0' and temp_str[1] == 'x':
        temp_str = temp_str.replace('0x', '')
        format_type = 16
    elif len(temp_str) > 2 and temp_str[0] == '0' and temp_str[1] == 'b':
        temp_str = temp_str.replace('0b', '')
        format_type = 2
    else:
        format_type = 10

    for s in temp_str:
        if s not in string.hexdigits:
            return None

    return int(temp_str, format_type)


def hex2strupper(val, t=32):
    """
    hex value to string upper
    in: [param] input value
    in: [t] input value bit len
    out: hex value string
    """
    if t == 8:
        format_val = '%02x'
    elif t == 16:
        format_val = '%04x'
    elif t == 64:
        format_val = '%016x'
    else:
        format_val = '%08x'

    s = format_val % val
    s = s.upper()
    s = '0x' + s

    return s


# Class ------------------------------------------------------------------------
class PartitionsItem:
    """
    Define Partitions Item Class
    """

    def __init__(self, inDict):
        self.name = inDict['name']
        self.attr = inDict['attribute']
        self.location = int(inDict['location']) if len(inDict['location']) else 0
        self.type = inDict['device']
        self.parent_dev = inDict['parent_dev'] if 'parent_dev' in inDict else None
        self.dev_name = inDict['name'] if self.attr == 'define' else inDict['device']
        self.page = str2hexval(inDict['page']) if len(inDict['page']) else 0
        self.begin = str2hexval(inDict['begin']) if len(inDict['begin']) else 0
        self.size = str2hexval(inDict['size']) if len(inDict['size']) else 0
        self.verify = self.begin + str2hexval(inDict['verify']) if len(inDict['verify']) else None
        self.end = self.begin + self.size - 1 if self.size else 0
        self.need_link = int(inDict['needlink']) if len(inDict['needlink']) else 0
        self.fal = int(inDict['fal']) if len(inDict['fal']) else 0

        # set define device type id
        if self.attr == 'define':
            self.start_index = 0
            self.dev_type = str2hexval(inDict['device'])
            self.dev_id = int(inDict['partindex']) if len(inDict['partindex']) else 0
        else:
            self.start_index = int(inDict['partindex']) if len(inDict['partindex']) else 0
            self.dev_type = str2hexval(inDict['dev_type']) if 'dev_type' in inDict else 0
            self.dev_id = str2hexval(inDict['dev_id']) if 'dev_id' in inDict else 0

    def getFirmwareInfoParam(self):
        fwInfoParam = ''
        falTableItem = ''

        fwInfoParamTemplate = PartitionsTemplateDict['def_fw_info_param']
        falTableItemTemplate = '    {FAL_PART_MAGIC_WORD, \"%s\", \"%s\", %s, %s, 0}, \\\n'

        name = self.name
        begin = hex2strupper(self.begin)
        end = hex2strupper(self.end)
        size = hex2strupper(self.size)
        page = hex2strupper(self.page)
        verify = hex2strupper(self.verify) if self.verify is not None else hex2strupper(0)
        need_verify = 1 if self.verify is not None else 0

        fwInfoParam += fwInfoParamTemplate % (self.name, name,
                                              self.dev_name,
                                              begin, end, verify, size, page,
                                              self.dev_type, self.dev_id, self.location,
                                              self.start_index,
                                              need_verify) + '\n\n'

        if self.fal:
            offset = hex2strupper(self.begin - self.parent_dev.begin)
            falTableItem += falTableItemTemplate % (self.name, self.dev_name, offset, size)

        return [fwInfoParam, falTableItem]

    def isBootPartition(self):
        return 1 if self.attr == 'boot' else 0

    def isAppPartition(self):
        return 1 if self.attr == 'app' else 0

    def isDownloadPartition(self):
        return 1 if self.attr == 'download' else 0

    def isInternalStorage(self):
        return 1 if self.location == 0 else 0

    def isExternStorage(self):
        return 1 if self.location else 0

    def isValid(self):
        return 1 if self.attr != 'define' else 0


class PartitionsInfo:
    """
    Define Partitions Info
    """

    def __init__(self, path, file_name):
        self.pdict = {}
        self.pList = []

        self.table_title = PartitionsTableList

        self.template = PartitionsTemplateDict
        self.template['def_define'] = '%s=0x%08xUL'
        self.template['def_fal_dev'] = '    &mcu_internal_flash%s, \\\n'

        self.path = path
        file_name = self.path + '\\' + file_name

        # print(self.path, file_name)

        self.__read_file(file_name)

    def __enter__(self):
        return self

    def __exit__(self, err_type, value, trace):
        if err_type is not None:
            print("type:", err_type)

        if value is not None:
            print("value:", value)

        if trace is not None:
            print("trace:", trace)

    def append(self, item):
        self.pdict[item.name] = item
        self.pList.append(item)

    def is_in(self, param):
        return True if param in self.pdict else False

    def get(self, param):
        return self.pdict[param]

    def get_list(self):
        return self.pList

    def get_dict(self):
        return self.pdict

    def get_len(self):
        return len(self.pList)

    def __read_file(self, file_name):
        def __is_part_line(__row):
            if len(__row) != len(self.table_title):
                print('partitions format error')
                return False

            for __ins in range(0, len(__row)):
                if not isinstance(__row[__ins], str):
                    print('partitions format error:%d' % __ins)
                    return False

            return True

        def __is_part_head(info):
            __row = []

            for s in info:
                if len(s):
                    __temp = s.replace(' ', '')
                    __temp = __temp.lower()
                    __row.append(__temp)
                else:
                    return False

            __title = PartitionsAttrDict

            for __ins in range(0, len(__title)):
                if __row[__ins] not in __title or __ins != __title[__row[__ins]]:
                    print('partitions attribute not have %s' % __row[__ins])
                    return False

            return True

        # file status check
        if os.path.isfile(file_name) and not os.access(file_name, os.R_OK):
            print(file_name + ' can not read')
            return None
        elif not os.path.isfile(file_name):
            print(file_name + ' is not exits')
            return None

        # get part info from file
        isHead = 0
        title = PartitionsAttrDict

        with open(file_name) as file:
            f_csv = csv.reader(file)

            for row in f_csv:
                if not __is_part_line(row):
                    continue

                if isHead == 1:
                    itemDict = {}
                    for ins in range(0, len(title)):
                        tempStr = row[ins]

                        if len(tempStr):
                            tempStr = tempStr.replace(' ', '')
                            tempStr = tempStr.lower()

                        # quick process
                        if title['begin'] == ins and len(tempStr) == 0:
                            if len(self.pList):
                                lastItem = self.pList[-1]

                                if lastItem.end is not None:
                                    tempStr = hex(lastItem.end + 1)
                                else:
                                    print('partitions format error')
                            else:
                                print('partitions format error')
                                continue
                        elif title['size'] == ins and len(tempStr) == 0:
                            if len(self.pList):
                                lastItem = self.pList[-1]
                                if lastItem.size is not None:
                                    tempStr = hex(lastItem.size)
                                else:
                                    print('partitions format error')
                            else:
                                print('partitions format error')
                                continue

                        index = self.table_title[ins]
                        itemDict[index[0]] = tempStr

                    # define type process
                    if itemDict['attribute'] != 'define':
                        for devType in self.pList:
                            if devType.attr == 'define' and devType.name == itemDict['device']:
                                itemDict['dev_id'] = str(devType.dev_id)
                                itemDict['dev_type'] = str(devType.dev_type)
                                itemDict['parent_dev'] = devType

                                if len(itemDict['page']) == 0:
                                    itemDict['page'] = str(devType.page)

                    # writer to buffer
                    self.append(PartitionsItem(itemDict))
                else:
                    isHead = __is_part_head(row)
        return self

    def get_partitons_info(self, name):
        if name in self.pdict:
            __part = self.pdict[name]
            __dev = self.pdict[__part.dev_name]
            return [__dev, __part]
        else:
            return None

    def generate_define_list(self):
        defines = []
        def_template = self.template['def_define']

        for item in self.pList:
            temp_string = 'FIRMWARE_' + item.name.upper() + '_BEGIN_ADDR'
            temp_define = def_template % (temp_string, hex2strupper(item.begin))
            defines.append(temp_define)
            # print(temp_define)

            temp_string = 'FIRMWARE_' + item.name.upper() + '_END_ADDR'
            temp_define = def_template % (temp_string, hex2strupper(item.end))
            defines.append(temp_define)
            # print(temp_define)

            if item.verify is not None:
                temp_string = 'FIRMWARE_' + item.name.upper() + '_VERIFY_ADDR'
                temp_define = def_template % (temp_string, hex2strupper(item.verify))
                defines.append(temp_define)
                # print(temp_define)

            if item.page > 0:
                temp_string = 'FIRMWARE_' + item.name.upper() + '_PAGE_SIZE'
                temp_define = def_template % (temp_string, hex2strupper(item.page))
                defines.append(temp_define)
                # print(temp_define)

            temp_string = 'FIRMWARE_' + item.name.upper() + '_AREA_SIZE'
            temp_define = def_template % (temp_string, hex2strupper(item.size))
            defines.append(temp_define)
            # print(temp_define)

        return defines

    def generate_define_string(self):
        macroTemplate = self.template['def_h_macro']

        defines = ''
        for item in self.pList:
            tempFormat = '//! Define ' + item.name.upper() + ' Partitions Info'
            defines += tempFormat + '\n'

            macro_string = 'FIRMWARE_' + item.name.upper() + '_BEGIN_ADDR'
            tempFormat = macroTemplate % (macro_string, hex2strupper(item.begin)) + 'UL'
            defines += tempFormat + '\n'
            # print(tempFormat)

            macro_string = 'FIRMWARE_' + item.name.upper() + '_END_ADDR'
            tempFormat = macroTemplate % (macro_string, hex2strupper(item.end)) + 'UL'
            defines += tempFormat + '\n'
            # print(tempFormat)

            if item.verify is not None:
                macro_string = 'FIRMWARE_' + item.name.upper() + '_VERIFY_ADDR'
                tempFormat = macroTemplate % (macro_string, hex2strupper(item.verify)) + 'UL'
                defines += tempFormat + '\n'
                # print(tempFormat)

            if item.page > 0:
                macro_string = 'FIRMWARE_' + item.name.upper() + '_PAGE_SIZE'
                tempFormat = macroTemplate % (macro_string, hex2strupper(item.page)) + 'UL'
                defines += tempFormat + '\n'
                # print(tempFormat)

            macro_string = 'FIRMWARE_' + item.name.upper() + '_AREA_SIZE'
            tempFormat = macroTemplate % (macro_string, hex2strupper(item.size)) + 'UL'
            defines += tempFormat + '\n'
            # print(tempFormat)

            defines += '\n'

        return defines[:-2] if len(defines) else defines

    def __generate_firmware_info_data(self):
        def __generate_fal_dev_and_func_code(__param):
            code_template = self.template['def_fal_func']
            dev_template = self.template['def_fal_dev']
            export_dev_template = self.template['def_export_fal_internal_dev']

            internal_list = __param['internal_stroage_list']
            fal_code = ''
            fal_dev = ''
            export_dev = ''

            for ins in range(0, len(internal_list)):
                def_index = '' if ins == 0 else str(ins)
                __item = self.pdict[internal_list[ins]]

                fal_dev += dev_template % def_index
                export_dev += export_dev_template.format(def_index) + '\n\n'

                fal_code += code_template.format(def_index, __item.dev_name,
                                                 hex2strupper(__item.begin),
                                                 hex2strupper(__item.size),
                                                 hex2strupper(__item.page),
                                                 area_begin='{', area_end='}') + '\n\n'

            if len(fal_dev):
                fal_dev = fal_dev[:-1]

            if len(export_dev):
                export_dev = export_dev[:-2]

            if len(fal_code):
                fal_code = fal_code[:-2]
            else:
                fal_code = '//! nothing!'

            return [fal_dev, export_dev, fal_code]

        def __generate_head_file(__file_name, __param):
            return self.template['def_fw_info_h'] % (__file_name, getCurTimeString(0),
                                                     __param['defines_string'],
                                                     len(__param['stroage_list']) + 1,
                                                     len(self.pList),
                                                     len(__param['internal_stroage_list']),
                                                     __param['boot_part_cnt'],
                                                     __param['app_part_cnt'],
                                                     __param['dl_part_cnt'],
                                                     __param['fal_device'],
                                                     __param['fal_table'],
                                                     __param['export_fal_internal_device'])

        def __generate_src_file(__file_name, __param):
            return self.template['def_fw_info_c'] % (__file_name, getCurTimeString(0),
                                                     __param['part_param'],
                                                     __param['fal_func_code'])

        falPartTableParamTemplate = '    {FAL_PART_MAGIC_WORD, \"firmware\", \"mcu_flash\", %s, %s, 0}, \\\n'

        param = {'defines_string': self.generate_define_string(), 'stroage_list': [], 'internal_stroage_list': [],
                 'fal_device': '', 'fal_table': '', 'part_param': '', 'fal_func_code': '', 'boot_part_cnt': 0,
                 'app_part_cnt': 0, 'dl_part_cnt': 0}

        # foreach list
        for item in self.pList:
            item_param = item.getFirmwareInfoParam()

            param['part_param'] += item_param[0]
            param['fal_table'] += item_param[1]

            param['boot_part_cnt'] += item.isBootPartition()
            param['app_part_cnt'] += item.isAppPartition()
            param['dl_part_cnt'] += item.isDownloadPartition()

            if item.isValid() and item.isExternStorage() and item.dev_name not in param['stroage_list']:
                param['stroage_list'].append(item.dev_name)

            if item.isValid() and item.isInternalStorage() and item.dev_name not in param['internal_stroage_list']:
                param['internal_stroage_list'].append(item.dev_name)

        # fal part code
        if len(param['part_param']) > 2:
            param['part_param'] = param['part_param'][:-2]

        # fal table
        if len(param['fal_table']):
            param['fal_table'] = param['fal_table'][:-1]
        else:
            param['fal_table'] = falPartTableParamTemplate % (hex2strupper(0),
                                                              hex2strupper(self.pdict['mcu_flash'].size))

        # internal stroage list
        if len(param['internal_stroage_list']) == 0:
            param['internal_stroage_list'].append(self.pdict['mcu_flash'])

        # fal device and function code
        fal_dev_code = __generate_fal_dev_and_func_code(param)
        param['fal_device'] = fal_dev_code[0]
        param['export_fal_internal_device'] = fal_dev_code[1]
        param['fal_func_code'] = fal_dev_code[2]

        # generate file data
        file_name = ['firmware_info.h', 'firmware_info.c']
        return [[file_name[0], __generate_head_file(file_name[0], param)],
                [file_name[1], __generate_src_file(file_name[1], param)]]

    def __generate_iar_icf_data(self, part):
        icfRegionSymbol = ''
        icfSpecials = ''
        icfRegion = ''
        icfSize = ''
        icfPlace = ''

        icfRegionSymbol += self.template['def_rom'] % (hex2strupper(part.begin),
                                                       hex2strupper(part.end)) + '\n'
        icfRegion += self.template['def_region'].format('ROM') + '\n'
        icfSpecials += self.template['def_intvec'] % (hex2strupper(part.begin)) + '\n'

        for item in self.pList:
            if item.attr == 'define':
                if item.name == 'mcu_ram':
                    icfRegionSymbol += self.template['def_ram'] % (hex2strupper(item.begin),
                                                                   hex2strupper(item.end)) + '\n'

                    icfRegion += self.template['def_region'].format('RAM') + '\n'
                elif item.name == 'stack':
                    icfSize += self.template['def_stack_size'] % (hex2strupper(item.size)) + '\n'
                elif item.name == 'heap':
                    icfSize += self.template['def_heap_size'] % (hex2strupper(item.size)) + '\n'
                else:
                    icfRegionSymbol += self.template['def_symbol'] % (item.name.upper() + '_start',
                                                                      hex2strupper(item.begin)) + '\n'

                    icfRegionSymbol += self.template['def_symbol'] % (item.name.upper() + '_end',
                                                                      hex2strupper(item.end)) + '\n'
                    icfRegion += self.template['def_region'].format(item.name.upper()) + '\n'
            elif part.attr == 'app' and item.attr == 'boot':
                icfRegionSymbol += self.template['def_symbol'] % (item.name.upper() + '_start',
                                                                  hex2strupper(item.begin)) + '\n'

                icfRegionSymbol += self.template['def_symbol'] % (item.name.upper() + '_end',
                                                                  hex2strupper(item.end)) + '\n'

                icfRegion += self.template['def_region'].format(item.name.upper()) + '\n'

                icfPlace += self.template['def_place_in_section'] % (item.name.upper(),
                                                                     'readonly',
                                                                     'BOOTLOADER') + '\n\n'

        if part.verify is not None:
            icfPlace += self.template['def_place_at_section'] % (hex2strupper(part.verify),
                                                                 'readonly',
                                                                 '.checksum') + '\n\n'

        icfSpecials = icfSpecials.strip('\n') if len(icfSpecials) else icfSpecials
        icfRegionSymbol = icfRegionSymbol.strip('\n') if len(icfRegionSymbol) else icfRegionSymbol
        icfSize = icfSize.strip('\n') if len(icfSize) else icfSize
        icfRegion = icfRegion.strip('\n') if len(icfRegion) else icfRegion
        icfPlace = icfPlace.strip('\n') if len(icfPlace) else icfPlace

        return self.template['def_icf'] % (icfSpecials, icfRegionSymbol, icfSize, icfRegion, icfPlace)

    def generate_firmware_info_file(self, target_path=''):
        if self.get_len() == 0:
            return

        for file_data in self.__generate_firmware_info_data():
            file_name = self.path + target_path + '\\' + file_data[0]
            # print(file_name)
            if len(file_data[1]):
                os.chmod(file_name, stat.S_IWRITE)
                with open(file_name, 'w') as file:
                    file.write(file_data[1])
                os.chmod(file_name, stat.S_IREAD)

    def generate_iar_icf_file(self, target_path=''):
        if self.get_len() == 0:
            return

        for item in self.pList:
            if item.isValid() and item.need_link != 0:
                file_name = self.path + target_path + '\\' + item.attr + '_link.icf'
                # print(file_name)
                with open(file_name, 'w') as file:
                    file.write(self.__generate_iar_icf_data(item))

# test code
# with PartitionsInfo('partitions.csv') as info:
#     info.generate_firmware_info_file()
#     info.generate_iar_icf_file('./linker_scripts/')

# import partitions
# help(partitions)
